FRIDA 高级 API:Frida Hook Java(1&2)、Frida hook native
Java.use() 进阶用法
在 FRIDA 高级逆向中,Java.use() 是 Hook Java 层代码的核心 API。除了常见的 Hook 普通类和方法外,它还支持处理枚举类、内部类和匿名类等复杂 Java 结构。
Hook 枚举类
枚举类在 Android 开发中广泛使用,例如定义状态码、类型标识等。使用 Java.use() Hook 枚举类时,需要注意枚举的静态实例是通过类初始化块创建的。
Java.perform(function () {
// 获取枚举类
var EnumClass = Java.use("com.example.app.StatusEnum");
// 遍历枚举值
var values = EnumClass.$new().values();
for (var i = 0; i < values.length; i++) {
console.log("枚举值: " + values[i].name() + " = " + values[i].ordinal());
}
// Hook 枚举的静态方法
EnumClass.valueOf.implementation = function (name) {
console.log("valueOf 被调用: " + name);
var result = this.valueOf(name);
console.log("返回值: " + result.name());
return result;
};
});
Hook 内部类
Java 内部类在编译后会生成独立的 class 文件,命名规则为 外部类$内部类。在 FRIDA 中直接使用 $ 符号引用即可。
Java.perform(function () {
// Hook 公有内部类
var InnerClass = Java.use("com.example.app.OuterClass$InnerClass");
InnerClass.secretMethod.implementation = function (arg) {
console.log("内部类方法被调用,参数: " + arg);
var result = this.secretMethod(arg);
console.log("内部类方法返回: " + result);
return result;
};
// Hook 私有内部类(同理,Frida 可以访问私有类)
var PrivateInner = Java.use("com.example.app.OuterClass$PrivateInner");
PrivateInner.getSecretData.implementation = function () {
var result = this.getSecretData();
console.log("私有内部类返回的密钥: " + result);
return result;
};
});
Hook 匿名类
匿名内部类没有显式名称,编译器会自动编号,命名格式为 外部类$数字。Hook 匿名类时需要先枚举类名来定位。
Java.perform(function () {
// 通过枚举找到匿名类
Java.enumerateLoadedClasses({
onMatch: function (className) {
if (className.indexOf("com.example.app.MainActivity$") !== -1) {
console.log("找到内部/匿名类: " + className);
}
},
onComplete: function () {
console.log("枚举完成");
}
});
// 直接 Hook 已知编号的匿名类
var AnonymousClass = Java.use("com.example.app.MainActivity$1");
AnonymousClass.onClick.implementation = function (v) {
console.log("匿名类的 onClick 被触发");
this.onClick(v);
};
});
Java.perform 的线程安全与回调队列
线程安全问题
Java.perform() 内部的代码运行在 Frida 创建的特殊线程上,而许多回调(如 Interceptor 的 Native Hook 回调)运行在目标线程上。直接在这些回调中调用 Java API 会导致线程不安全错误。
// 错误示例:在 Native 回调中直接调用 Java API
Interceptor.attach(baseAddr.add(0x1234), {
onEnter: function (args) {
// 这里运行在 Native 线程,不能直接使用 Java.use()
var clazz = Java.use("com.example.app.Util"); // 可能崩溃!
}
});
// 正确示例:通过 Java.scheduleOnMainThread() 调度
Interceptor.attach(baseAddr.add(0x1234), {
onEnter: function (args) {
var arg0 = args[0].toInt32();
// 将 Java 操作调度到主线程
Java.scheduleOnMainThread(function () {
Java.perform(function () {
var Util = Java.use("com.example.app.Util");
var result = Util.process(arg0);
console.log("Java 处理结果: " + result);
});
});
}
});
回调队列机制
Java.perform() 还有一个重要特性——回调队列。如果在 Java 虚拟机尚未完全初始化时调用 Java.perform(),它不会立即执行回调函数,而是将回调加入队列,等待 VM 就绪后依次执行。
// 即使 VM 还没准备好,Java.perform 也会自动排队
function hookJavaClass() {
Java.perform(function () {
console.log("Hook Java 类执行中...");
var Target = Java.use("com.example.app.Target");
Target.verify.implementation = function (token) {
console.log("拦截到 token 验证: " + token);
return true; // 绕过验证
};
});
}
// 在脚本加载时调用,无论 VM 是否就绪都能正常工作
hookJavaClass();
Interceptor.attach() Hook Native 函数
Interceptor 是 Frida 中最强大的 Native Hook 工具,它可以在函数的入口和出口处插入自定义代码,实现对 Native 函数参数的读取、修改以及对返回值的篡改。
基本用法
var baseAddr = Module.findBaseAddress("libnative.so");
if (baseAddr) {
var funcAddr = baseAddr.add(0x1A2B); // 目标函数偏移
Interceptor.attach(funcAddr, {
onEnter: function (args) {
// 函数入口,读取参数
console.log("[+] 函数被调用");
console.log(" arg0 (int): " + args[0].toInt32());
console.log(" arg1 (ptr): " + args[1]);
console.log(" arg2 (str): " + args[2].readUtf8String());
},
onLeave: function (retval) {
// 函数出口,读取/修改返回值
console.log(" 返回值: " + retval);
retval.replace(0); // 修改返回值为 0
}
});
}
onEnter 参数详解
onEnter 回调接收一个 args 数组,对应函数的参数列表。在 ARM64 下,前 8 个参数通过寄存器 x0-x7 传递,后续参数通过栈传递。在 ARM32 下,前 4 个参数通过 r0-r3 传递。
onLeave 参数详解
onLeave 回调接收一个 retval 对象,代表函数的返回值。对于整数返回值使用 retval.toInt32() 或 retval.toUInt32() 读取,对于指针返回值使用 retval.readPointer() 读取。
读取和修改寄存器参数
除了通过 args 数组访问参数外,Frida 还提供了通过 this.context 直接读写 CPU 寄存器的能力,这在分析未识别参数类型的函数时非常有用。
ARM64 寄存器操作
Interceptor.attach(funcAddr, {
onEnter: function (args) {
var ctx = this.context;
// 读取 ARM64 参数寄存器
console.log("x0 = " + ctx.x0); // 第1个参数
console.log("x1 = " + ctx.x1); // 第2个参数
console.log("x2 = " + ctx.x2); // 第3个参数
console.log("x3 = " + ctx.x3); // 第4个参数
console.log("x4 = " + ctx.x4); // 第5个参数
console.log("x5 = " + ctx.x5); // 第6个参数
console.log("x6 = " + ctx.x6); // 第7个参数
console.log("x7 = " + ctx.x7); // 第8个参数
console.log("sp = " + ctx.sp); // 栈指针
console.log("pc = " + ctx.pc); // 程序计数器
// 修改寄存器值
ctx.x0 = ptr(0xDEADBEEF); // 修改第1个参数
},
onLeave: function (retval) {
var ctx = this.context;
// x0 在返回时存放返回值
console.log("返回值 (x0) = " + ctx.x0);
}
});
ARM32 寄存器操作
Interceptor.attach(funcAddr, {
onEnter: function (args) {
var ctx = this.context;
// 读取 ARM32 参数寄存器
console.log("r0 = " + ctx.r0); // 第1个参数
console.log("r1 = " + ctx.r1); // 第2个参数
console.log("r2 = " + ctx.r2); // 第3个参数
console.log("r3 = " + ctx.r3); // 第4个参数
console.log("sp = " + ctx.sp); // 栈指针
console.log("lr = " + ctx.lr); // 返回地址
console.log("pc = " + ctx.pc); // 程序计数器
// 修改参数
ctx.r0 = ptr(0x12345678);
},
onLeave: function (retval) {
// ARM32 的返回值也在 r0 中
console.log("返回值 (r0) = " + this.context.r0);
}
});
修改返回值
修改返回值是 Hook 中最常见的操作之一,用于绕过校验逻辑、改变程序行为等。
// 场景:绕过签名校验
Interceptor.attach(checkSignatureAddr, {
onEnter: function (args) {
console.log("[签名校验] 被调用");
},
onLeave: function (retval) {
// 0 表示校验通过,1 表示失败
console.log("[签名校验] 原始返回值: " + retval.toInt32());
retval.replace(0); // 强制返回"通过"
console.log("[签名校验] 已修改为: 0");
}
});
// 场景:修改布尔返回值
Interceptor.attach(isVipAddr, {
onLeave: function (retval) {
console.log("[VIP 检查] 原始值: " + retval.toInt32());
retval.replace(1); // 0=false, 1=true
console.log("[VIP 检查] 已修改为: 1 (VIP)");
}
});
替换整个 Native 函数实现
除了在函数前后插入代码外,Frida 还支持使用 Interceptor.replace() 完全替换一个函数的实现。这在需要对函数进行大范围修改时非常有用。
// 使用 NativeFunction 定义新的函数实现
var newImpl = new NativeFunction(function (param1, param2) {
console.log("[替换函数] param1=" + param1 + ", param2=" + param2);
// 完全自定义的逻辑
var result = param1 * param2 + 0x1337;
console.log("[替换函数] 计算结果: " + result);
return result;
}, 'int', ['int', 'int']);
// 替换原函数
var targetAddr = baseAddr.add(0x2A3B);
Interceptor.replace(targetAddr, newImpl);
// 也可以替换为另一个已有函数的地址(函数指针转发)
// Interceptor.replace(targetAddr, anotherFuncAddr);
replace 与 attach 的区别
| 特性 | Interceptor.attach |
Interceptor.replace |
|---|---|---|
| 原函数 | 保留,可在前后插入代码 | 完全替换,原函数不再执行 |
| 调用原函数 | 直接调用 this.func() |
无法调用原函数 |
| 适用场景 | 监控参数、修改返回值 | 重写整个函数逻辑 |
| 性能影响 | 较小 | 较小 |
Hook 导出函数和非导出函数
Hook 导出函数
导出函数可以通过函数名直接定位,使用 Module.getExportByName() 获取地址。
// 通过函数名获取导出函数地址
var openAddr = Module.getExportByName("libc.so", "open");
var strcmpAddr = Module.getExportByName("libnative.so", "check_password");
console.log("[*] open 地址: " + openAddr);
console.log("[*] check_password 地址: " + strcmpAddr);
// Hook 导出函数
Interceptor.attach(strcmpAddr, {
onEnter: function (args) {
console.log("[strcmp] s1=" + args[0].readUtf8String() +
" s2=" + args[1].readUtf8String());
},
onLeave: function (retval) {
console.log("[strcmp] 返回值: " + retval.toInt32());
}
});
Hook 非导出函数
非导出函数在 ELF 的动态符号表中不可见,需要通过 IDA Pro 分析确定偏移量后,通过基地址+偏移的方式定位。
// 第一步:获取模块基地址
var mod = Process.findModuleByName("libnative.so");
if (mod) {
var base = mod.base;
console.log("[*] libnative.so 基地址: " + base);
// 第二步:使用 IDA 分析出的偏移量定位非导出函数
// 假设在 IDA 中看到函数地址为 0x1A4C(相对 SO 文件偏移)
var decryptFunc = base.add(0x1A4C);
console.log("[*] 解密函数实际地址: " + decryptFunc);
// 第三步:Hook 非导出函数
Interceptor.attach(decryptFunc, {
onEnter: function (args) {
var ctx = this.context;
console.log("[解密函数] 被调用");
console.log(" 输入指针: " + args[0]);
console.log(" 输入长度: " + ctx.x1);
// 读取输入数据
var len = ctx.x1.toInt32();
if (len > 0 && len < 1024) {
console.log(" 输入数据(hex): " +
hexdump(args[0], { length: len }));
}
},
onLeave: function (retval) {
console.log("[解密函数] 返回: " + retval.toInt32());
}
});
}
处理 ASLR(地址随机化)
Android 系统启用了 ASLR,每次运行时模块基地址会变化。因此,非导出函数的地址必须动态计算,不能硬编码绝对地址。
// 安全的 Hook 模式:每次运行时动态计算
function hookNonExportedFunc(moduleName, offset) {
var mod = Process.findModuleByName(moduleName);
if (!mod) {
console.log("[-] 模块未加载: " + moduleName);
return;
}
var addr = mod.base.add(offset);
console.log("[+] Hook 目标: " + moduleName +
" + 0x" + offset.toString(16) +
" = " + addr);
Interceptor.attach(addr, {
onEnter: function (args) {
console.log("[+] 函数调用 @ " + addr);
}
});
}
// 使用示例
hookNonExportedFunc("libnative.so", 0x1A4C);
hookNonExportedFunc("libnative.so", 0x2B8D);
hookNonExportedFunc("libsecurity.so", 0x4567);
总结
本文介绍了 FRIDA 高级逆向中最核心的两组 API:Java Hook 和 Native Hook。Java.use() 配合 Java.perform() 可以深入 Hook Java 层的各种复杂类结构,而 Interceptor.attach() 则提供了对 Native 函数的细粒度控制——从读写寄存器参数到修改返回值、替换函数实现。掌握这些 API 是进行高级逆向分析的必备技能,后续文章将在此基础上介绍 Stalker 代码追踪和 OLLVM 混淆分析等进阶技术。